/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package routefinder;

import routefinder.algorithm.Dijkstra;
import routefinder.algorithm.MyBFSTrips;
import routefinder.algorithm.MyBFS;
import routefinder.exception.RouteFinderException;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Logger;

/**
 * This class behaves as a facade to the Dijkstra Engine.
 * 
 * @author Lucas
 */
public class RouteFinderExtendedImpl implements RouteFinder {

    private static final Logger LOG = Logger.getLogger(RouteFinderExtendedImpl.class.getName());
    private static RouteFinderExtendedImpl instance;
    private Dijkstra engine;
    private City previousStartCity;
    private IRoutesMap routesMap;

    private RouteFinderExtendedImpl() {
    }

    public static RouteFinderExtendedImpl getInstance() {
        if (instance == null) {
            instance = new RouteFinderExtendedImpl();
        }
        return instance;
    }

    /**
     * Sets the graph.
     *
     * @param routesMap
     */
    public void setRoutesMap(IRoutesMap routesMap) {
        this.routesMap = routesMap;
    }

    /**
     * @param citiesAsString CSV of cities representing a route. Ex. A,B,C
     * @return
     */
    public int getRouteDistance(String routeListAsString) { //TODO move this code.
        LOG.info("getRouteDistance: " + routeListAsString);
        StringTokenizer st = new StringTokenizer(routeListAsString, ",");
        Route route = null;
        boolean isReachable = false;
        while (st.hasMoreTokens()) {
            String cityName = st.nextToken();
            City cityInRoute = new City(cityName);
            if (route == null) {
                route = new Route();
                route.addStop(cityInRoute, 0); //the 1st element will not have distance
            } else {
                isReachable = false;
                for (Link v : routesMap.getDestinations(route.getLastStop())) {
                    if (v.getCity().getName().equals(cityInRoute.getName())) {
                        isReachable = true;
                        route.addStop(cityInRoute, v.getDistance());
                        break; // iterate through next cityInRoute
                    }
                }
                if (!isReachable) {
                    break; // one of the cities in the route list can't be reached on the map.
                }
            }
        }

        int routeDistance = -1;
        if (isReachable) {
            routeDistance = route.getDistance();
        }
        return routeDistance;
    }

    /**
     * @see RouteFinderExtended#getTrips(routefinder.City, routefinder.City, int) 
     */
    public int getTrips(City startCity, City endCity, int maximumStops) {
        MyBFSTrips bfs = new MyBFSTrips(routesMap);
        int numberOfTrips = bfs.execute(startCity, endCity, maximumStops, false);
        return numberOfTrips;
    }

    /**
     * @see RouteFinderExtended#getAllShortestRouteLengths(routefinder.City) 
     */
    public Map<City, Integer> getAllShortestRouteLengths(City startCity) {
        runDijkstraEngine(startCity, null); // destination doesnt matter in this case
        return engine.getShortestDistances();
    }

    /**
     * @see RouteFinder#getShortestRouteLength(routefinder.City, routefinder.City)
     */
    public int getShortestRouteLength(City startCity, City endCity) {
        Integer d = getAllShortestRouteLengths(startCity).get(endCity);
        return (d == null) ? -1 : d;
    }

    private City checkForExistingCity(City startCity) throws RouteFinderException {
        final Map<City, City> currentCities = routesMap.getCities();
        City existingCityReference = currentCities.get(startCity);
        if (existingCityReference == null) {
            throw new RouteFinderException("City " + startCity + " doesn't exist on this map");
        }
        return existingCityReference;
    }

    private void runDijkstraEngine(City startCity, City destinationCity) {
        runDijkstraEngine(startCity, destinationCity, Integer.MAX_VALUE, false);
    }

    private void runDijkstraEngine(City startCity, City destinationCity, int stops, boolean isStrictNumberOfStops) {
        engine = new Dijkstra(routesMap, stops, isStrictNumberOfStops);
        City existingCityReference = checkForExistingCity(startCity);
        engine.execute(existingCityReference, destinationCity);
        this.previousStartCity = existingCityReference;
    }

    /**
     * @see RouteFinderExtended#getTripsStrictStops(previousStartCity, previousStartCity, requiredStops)
     */
    public int getTripsStrictStops(City startCity, City endCity, int requiredStops) {
        MyBFSTrips bfs = new MyBFSTrips(routesMap);
        int numberOfTrips = bfs.execute(new City("A"), new City("C"), 4, true);
        return numberOfTrips;
    }

    /**
     * @see RouteFinderExtended#getRoutesConstrainedDistance(routefinder.City, routefinder.City, int) 
     */
    public int getRoutesConstrainedDistance(City startCity, City endCity, int maximumDistance) {
        MyBFS bfs = new MyBFS(routesMap);
        int numberOfRoutes = bfs.execute(startCity, endCity, maximumDistance);
        return numberOfRoutes;
    }
}
